Skip to content

Go 缓存工具选型 ​

go-redis、rueidis、ristretto、bigcache、otter 经常同时出现在缓存推荐清单里,但前两个主要负责与 Redis 通信,后三个把数据留在当前 Go 进程。把五个名字排成一个速度榜,先把问题分错了层。

这篇文章先区分远程客户端与本地缓存,再逐个解释它们的入口、数据语义、内存管理和适用条件。最值得检查的几个行为都实际跑过:Redis 未命中怎么表示,客户端缓存是否命中,Ristretto 的 Set 成功是否等于保留,BigCache 超过 LifeWindow 是否还返回值,以及 Otter 的 loader 是否复用已有结果。

固定版本,不按浮动 README 猜 API。 2026-10-13 通过官方 Go 模块代理解析并固定下面版本,依赖经过模块校验;实验使用 Go 1.27.2、Linux/amd64 和临时 Redis 8.2.2。表中的版本是本文证据边界,不是承诺今后仍为最新或最适合你的版本。

库本文模块版本该版本 go.mod 最低 Go 版本主要工作层
go-redisgithub.com/redis/go-redis/v9 v9.23.01.26.0Redis 客户端,含连接池与多种部署入口
rueidisgithub.com/redis/rueidis v1.0.781.25.0Redis 客户端,自动流水线与客户端缓存
Ristrettogithub.com/dgraph-io/ristretto/v2 v2.4.21.24.0以成本预算和准入策略管理本地缓存
BigCachegithub.com/allegro/bigcache/v3 v3.2.01.22以字节存储降低大量对象的 GC 扫描负担
Ottergithub.com/maypok86/otter/v2 v2.3.01.24.0泛型本地缓存,支持加载、过期和刷新

最低版本取自固定模块的 go.mod,而非 README 中可能尚未同步的安装说明。完整版本和 go.sum 在文末。

先决定要省哪一次工作 ​

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    A["业务代码"] --> L["可选 L1 本地缓存<br/>Ristretto / BigCache / Otter"]
    L -->|"未命中"| C["Redis 客户端<br/>go-redis / rueidis"]
    C -->|"网络请求"| R["Redis L2"]
    R -->|"L2 未命中,由应用加载"| D["数据库/其他权威源"]
    R -. "Tracking 失效推送" .-> CC["客户端自带本地缓存<br/>仅启用该功能时"]
    CC -. "可直接复用响应" .-> C
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    A["业务代码"] --> L["可选 L1 本地缓存<br/>Ristretto / BigCache / Otter"]
    L -->|"未命中"| C["Redis 客户端<br/>go-redis / rueidis"]
    C -->|"网络请求"| R["Redis L2"]
    R -->|"L2 未命中,由应用加载"| D["数据库/其他权威源"]
    R -. "Tracking 失效推送" .-> CC["客户端自带本地缓存<br/>仅启用该功能时"]
    CC -. "可直接复用响应" .-> C

普通本地缓存可以保存任意计算结果,自己决定如何失效;Redis client-side caching 保存的是 Redis 查询响应,并由 Tracking 协议辅助失效。两者都在本进程里,却有不同的数据来源与一致性协议。Redis Tracking

选择问题倾向的起点需要付出的成本
主要是可靠、易维护地访问 Redis先比较 go-redis 与 rueidis 的 API、重试和部署支持网络、连接生命周期、故障路由、协议兼容
热点 Redis 读需要减少往返评估客户端自带缓存及其失效契约本地内存、失效延迟、重连清理、版本限制
缓存任意 Go 计算结果Ristretto 或 Otter 等本地缓存每实例一份、进程重启丢失、自己处理共享对象与失效
大量序列化字节导致对象管理负担把 BigCache 纳入实测编解码成本、TTL 语义、条目与分片上限
数据根本不能丢先选择权威存储与一致性协议这些缓存库不能单独满足持久数据要求

别同时把同一份内容放进业务 L1、客户端缓存和 Redis,却没有明确谁负责失效。多加一层可能减少请求,也可能增加一层陈旧值。

go-redis:常用命令直接,部署入口明确 ​

常见入口是 redis.NewClient 连接单节点,NewClusterClient 处理 Cluster 路由,NewFailoverClient 根据 Sentinel 发现主节点。命令方法返回结果对象,再用 .Result() 同时取值与错误,或用 .Err() 只检查错误。复用长生命周期客户端,不要每个 HTTP 请求创建一个。v9.23.0 客户端源码、Cluster、Sentinel

文末配置显式给出连接、读写超时与连接池规模,并用请求 context 限制总等待。下面是其中的正常读取与未命中判断:

go
v, err := r.Get(ctx, "lab08:tools:g").Result()
_, missing := r.Get(ctx, "lab08:tools:missing").Result()
fmt.Printf("go-redis: value=%s missing=%v\n", v, errors.Is(missing, redis.Nil))
text
go-redis: value=value missing=true

redis.Nil 表示 Redis 返回空结果,不等于网络错误,也不等于正常的空字符串。完整程序检查了每条命令的错误;正文片段只突出这个区别。

连接池与超时要按总预算配置 ​

连接池解决复用与并发,不是越大越好。大池带来更多套接字、缓冲区和服务端连接状态;池满时请求还会等待。应用 200 ms 的总预算,不应该被内部几轮长超时和重试耗尽之后才发现失败。

v9.23.0 的 Options 中有 DialTimeout、ReadTimeout、WriteTimeout、PoolTimeout、PoolSize,以及控制 context 截止时间行为的 ContextTimeoutEnabled。应按所用版本检查零值和负值含义,不能把每个库的 0 都理解为关闭。固定版本 Options

例如 MaxRetries=-1 才是本版本禁用命令重试的取值,0 会使用默认重试配置。本文选择 -1 是为了让教学实验失败位置清楚,不代表生产都应禁用重试。

更重要的是重试语义。INCR 已在服务端执行但回复丢失时,应用看到的只是网络错误;再执行可能增加两次。客户端无法凭空知道所有不确定结果,应依业务幂等性、命令类别与客户端实现共同设计。对关键写,唯一业务请求号和原子去重往往比增加重试次数更重要。

批量与客户端缓存也要按当前版本看 ​

Pipeline / Pipelined 用于批量收发,TxPipeline / TxPipelined 把命令放进 Redis 事务;Watch 支持乐观并发检查。Pipeline 减少往返,不保证整批原子;事务运行时错误也不自动回滚。采用哪一个要对应服务端语义,而不是只看方法名中有没有 Tx。Pipeline、事务

也不能沿用旧印象说“go-redis 没有自动流水线或客户端缓存”。本文固定的 v9.23.0 已提供实验性的自动流水线 API,以及实验性的 ClientSideCacheConfig。其 README 明确约束客户端缓存目前使用 RESP3、单节点客户端、DB 0,并说明失效异步处理、读可能暂时陈旧且不保证单个调用者的读单调性。v9.23.0 功能说明

这些功能仍标为实验性,接口可能变化;本文没有执行 go-redis 的这两项实验功能。需要它们时,应先固定版本,再跑你实际用到的命令、连接模式与断连恢复测试,而不是根据另一版本的评测做决定。

rueidis:默认合并并发命令,显式区分可缓存读取 ​

rueidis 用 builder 构造命令,例如:

go
client.Do(ctx, client.B().Get().Key("item:7").Build()).ToString()

builder 把命令的键与参数结构显式表达出来,便于路由与命令属性处理。常用入口还有 DoMulti 批量执行、DoCache 执行可缓存查询、NewClient 创建客户端。与 go-redis 的方法风格相比,链式构造更长,是否更合适取决于团队阅读与封装方式。v1.0.78 公开接口

自动 Pipeline 省往返,但也可能引入排队 ​

本版本默认把并发的非阻塞命令自动放入流水线,不必业务代码手工收集一批请求。较高并发时,这能减少系统调用与网络等待。代价是请求与响应处理需要额外 goroutine,批次和连接上的排队可能影响单请求延迟;阻塞操作与长响应也需要按库的专用路径处理。自动流水线说明

因此“吞吐更高”与“每个请求延迟更低”不是同一命题。低并发、很大的响应、混合阻塞请求,可能得到不同结果。本版本可用 DisableAutoPipelining 改变默认策略,并有显式进入流水线路径的接口;本文没有做吞吐对比,不给出跨库倍数。

DoCache:缓存响应,并接受服务端失效 ​

本次用 Redis 8.2.2 验证连续两次可缓存读取:

go
first := u.DoCache(ctx, u.B().Get().Key("lab08:tools:r").Cache(), time.Minute)
second := u.DoCache(ctx, u.B().Get().Key("lab08:tools:r").Cache(), time.Minute)
fmt.Println(second.IsCacheHit())

完整程序实际输出:

text
rueidis: value=value second-hit=true

第二次命中了客户端内存,说明本次请求避免了再次取这个值。这里没有通过其他客户端修改键,所以不能据此宣称失效传播、重连清空和所有竞态都已经测试通过。缓存接口说明

DoCache 的时间参数限制客户端保留响应的时间,不是对服务端执行 EXPIRE。服务端可以保留键更久;客户端也会因失效或容量等原因更早移除响应。CacheSizeEachConn 是每个相关连接的本地缓存预算,多个节点或连接的总预算需要另外汇总,不能把它误当整个进程唯一上限。ClientOption

Do 系列接受 context,但请求超时返回不表示已经发送的服务端命令被撤销。本版本 DisableRetry 主要控制网络错误下只读命令的自动重试;业务侧仍要理解自己执行的是读取还是有副作用的命令。OnInvalidations 回调必须很快,不能把慢数据库查询塞进响应处理路径。重试与超时说明

本地缓存先比较语义,再比较吞吐 ​

维度Ristretto v2.4.2BigCache v3.2.0Otter v2.3.0
键和值泛型键/值string → []bytecomparable 泛型键、泛型值
容量控制MaxCost 与每条成本,配合准入/淘汰分片字节队列,HardMaxCacheSize 等MaximumSize 或 MaximumWeight + Weigher
哪些值会留下TinyLFU 准入、SampledLFU 淘汰受生命周期与空间回收影响自适应 W-TinyLFU 等策略管理保留
写完立刻可读Set 经缓冲/准入,不能普遍保证成功 Set 后可查,但仍受后续淘汰影响普通 Set / Get 有具体接口语义,容量维护仍非持久保证
过期入口SetWithTTLLifeWindow 与清理;普通 Get 不等于严格 TTL 检查ExpiryCalculator 可按写入/访问等规则
内置加载与刷新业务通常另封装业务另封装Get + Loader、刷新策略
最应核对的代价Set 丢弃、成本口径、维护缓冲编解码、非硬 TTL、分片与字节预算策略维护、加载/刷新错误和陈旧窗口

三者的数据都在当前进程里。多实例各有一份,重启后消失,不会因为“线程安全”就有跨进程一致性。线程安全通常只保护缓存内部结构;缓存的值如果含共享 map、slice、指针,调用者仍须约定不可变、复制或另外同步,避免在缓存外产生数据竞争。

Ristretto:会有意拒绝不值得保留的值 ​

Ristretto 把两个决定分开:新数据是否值得进入缓存,空间不足时移走谁。TinyLFU 根据近期频率信息做准入,SampledLFU 用采样方式选淘汰候选,目标是在有限成本预算下保留更可能再次被用到的数据。v2.4.2 README、策略实现

主要入口是 NewCache,然后 Set / SetWithTTL、Get、Del,结束时 Close。NumCounters 控制频率统计规模,MaxCost 是总成本预算,BufferItems 与读取统计缓冲有关。成本单位由应用设计,可以是近似字节,也可以是业务权重;给每项传 1 就不能把 MaxCost 直接宣称为字节上限。

Set 返回 true,仍然可能读不到 ​

本次配置 MaxCost=1024,尝试写一个成本为 10000 的值:

go
queued := r.Set("oversize", "value", 10000)
r.Wait()
_, hit := r.Get("oversize")

实际输出:

text
ristretto: queued=true retained=false

true 表示写入被接收到处理路径,不是承诺最终保留。Wait 等待写缓冲处理,之后仍读不到,因为这项成本超过预算,不能被接受。正常大小的新项也可能被准入策略拒绝;负载高时 Set 还可能直接返回 false。Set 与 Wait 契约

这种设计适合“没有缓存就重新计算”的场景,不能当成写后必须保存的 map、幂等记录库或锁服务。业务读取必须处理 miss;不能 Set 后默认下一行 Get 一定成功。批量预热也应检查最终命中与准入情况,而不是只统计调用 Set 的次数。

缓存内部维护、键与值自身占用、统计计数器和 Go 运行时都有额外成本。即使把 cost 设计成字节,MaxCost 也不是进程 RSS 的硬限额;传入的大小若不包含值所引用的对象图,还会低估真实占用。配置与成本逻辑

BigCache:把很多小对象变成较少的字节存储 ​

BigCache 用分片、哈希索引和字节队列管理条目,减少为每条业务数据建立大量可被 GC 扫描的指针对象。字节数组仍在 Go 堆上,缓存本身也仍有索引和管理对象,所以“完全没有 GC”不是准确说法。v3.2.0 存储与分片、字节队列

它接收 []byte,业务 struct 要自己编码,读取后再解码。这适合已经序列化的数据或很大的条目集合;如果对象很小、每次都要昂贵地编解码,省下的 GC 扫描未必抵得过这部分 CPU。

常用入口是 bigcache.New(ctx, config)、Set、Get、GetWithInfo、Delete 与 Close。Shards 应满足库的配置要求;MaxEntriesInWindow 和 MaxEntrySize 主要影响初始容量估计,不是“多一条或大一字节就报错”的通用配额。HardMaxCacheSize 针对缓存存储预算,不能代替进程级内存测量。Config

LifeWindow 不是普通 Get 的严格过期屏障 ​

本次把 LifeWindow 设为 1 秒,关闭周期清理,不再执行后续写入,等待 2.1 秒后读取:

go
bv, info, err := b.GetWithInfo("k")
fmt.Printf("bigcache: value=%s expired=%v\n", bv, info.EntryStatus == bigcache.Expired)
text
bigcache: value=value expired=true

数据仍在,GetWithInfo 告诉我们它已过期。查看本版本源码,普通 Get 读取尚未删除的条目时不会先按 LifeWindow 拒绝;GetWithInfo 才附带这个状态。清理与写入可能稍后删除过期条目。读取与过期判断源码

如果业务要求超过某时刻绝不能返回旧值,就要自己检查 EntryStatus 或存储业务到期时间,不能仅配置 LifeWindow。关闭清理的实验用来放大这个差异,不是建议在线上永久不清理。

BigCache 的哈希碰撞也值得知道:内部要核对实际键名,避免把另一个键的值当作当前键返回;碰撞可能造成原条目丢失,而不是为所有同哈希键提供完整冲突链。不要把它当成不会丢数据的权威 map,更不要在不了解分布的情况下替换默认哈希。碰撞处理

Otter:把加载和刷新纳入本地缓存接口 ​

Otter v2 使用泛型 Options[K,V]。New 返回配置错误,Must 在错误时 panic;服务初始化时应选择符合项目错误处理方式的入口。GetIfPresent 只读已存在项,Get(ctx,key,loader) 可在缺失时加载,Set / Invalidate 修改本地缓存。v2.3.0 API

本次构建容量为 10 的缓存,用一个计数 loader 连续读同一键两次:

go
loader := otter.LoaderFunc[string, string](func(context.Context, string) (string, error) {
    loads++
    return "value", nil
})
ov, err := o.Get(ctx, "k", loader)
_, err = o.Get(ctx, "k", loader)
text
otter: value=value loads=1

第一次加载后留在缓存,第二次复用值。这个实验是串行读取;库还支持对同键加载进行协调,但本次输出不能单独证明其全部并发与取消语义。采用时应补上与你的 loader 相匹配的并发测试。Loader 接口

容量是条数,还是自定义权重 ​

MaximumSize 限条数,MaximumWeight 配合 Weigher 限累计权重,二者不能同时使用。权重在插入或更新时计算;值所引用的内容后来原地变大,并不会自动重新称重。该版本还说明容量维护期间可能短暂超过阈值,因此不能把权重预算当成进程 OOM 前的绝对物理屏障。Options

它的自适应 W-TinyLFU 策略同时考虑近期和频率信息,适合把不同访问轨迹纳入测试。算法名本身不决定你的命中率:一次性扫描、少数长久热点和快速换代热点需要不同测试数据。固定版本策略说明

过期和刷新是两个时间条件 ​

ExpiryWriting 等 ExpiryCalculator 决定什么时候条目不可再用;RefreshCalculator 决定什么时候触发刷新。按写入计时与按访问重置期限有不同效果:后者可能让一个经常被读的旧值一直保留,需要确认这是否符合业务新鲜度要求。过期计算器

配置刷新后,首次符合刷新条件的读取可以触发后台加载,同时继续返回旧值,直到刷新完成。这是在延迟与陈旧之间取舍,不是提前完成了新鲜读取。v2.3.0 的配置文档说明刷新错误会记录并吞掉,因此业务不能只等前台 Get 报错来发现刷新长期失败;还要看日志、指标和最大陈旧期。刷新契约

缓存可能有后台维护任务。本文测试结束调用 StopAllGoroutines 以明确收束生命周期;该方法不等于把缓存项清空,清空另有相应接口。生产中按库版本的生命周期契约管理,避免在每个请求里反复创建缓存。

如何在这些库之间做有用的测试 ​

只测“循环读一个已经命中的小 key”主要说明最理想热路径,无法回答真实系统的问题。一个可用于决策的对比应把以下条件固定下来:

条件为什么影响结论
键分布与热点变更均匀、Zipf、扫描和热点迁移会改变命中与准入效果
值尺寸及编解码方式BigCache 的字节路径与泛型对象路径不能不计编解码就比较
读写比、过期与刷新写缓冲、失效和维护成本可能被纯读基准隐藏
容量与统计结构同条数不等于同字节;不同 cost 口径不可直接排名
并发、网络 RTT 与响应大小自动流水线、连接池与网络传输的收益随负载变化
冷启动和持续时间预热前后不同;短测可能没遇到清理、GC 或热点轮换
故障与取消断连、池满、重试、过期读取和等待者取消影响可用性

输出应同时报告有效吞吐、P50/P95/P99、命中率、准入/淘汰、后端调用数、CPU、分配与内存,而不是只报一个 ns/op。发生失败的请求不能悄悄从延迟统计中删掉;异步 Set 也不能只测入队速度,却把它当成最终可读吞吐。

本文没有做这个层面的基准,所以不评选“最快”。更实际的起点是:常规 Redis 访问先选最符合现有项目的客户端;需要客户端缓存时验证 Tracking 和重连;本地任意对象缓存比较 Ristretto 的准入语义与 Otter 的 loader/刷新;大量字节数据再验证 BigCache 的内存与编解码收益。每一步都对应一个明确问题。

依据与验证范围 ​

  • 固定版本官方项目:go-redis v9.23.0、rueidis v1.0.78、Ristretto v2.4.2、BigCache v3.2.0、Otter v2.3.0。各功能就近链接了具体 README、API 或实现文件。
  • 版本与 Go 最低要求:固定模块的 go.mod;下载时使用 proxy.golang.org 与 sum.golang.org,go mod verify 成功。
  • 服务端缓存失效背景:Redis client-side caching reference。
  • 实测:Go 1.27.2、Linux/amd64、Redis 8.2.2 libc 单节点;普通运行、test、race、vet 均成功,输出为下方完整程序产生。测试对这五项演示行为都有明确断言。
  • 未测:跨库性能排名、Cluster/Sentinel 切换、客户端缓存外部失效与重连、go-redis 实验性自动流水线/客户端缓存、Otter 并发加载与后台刷新。本文只把这些功能作为固定版本文档事实,不把未跑的场景写成实测。

完整程序、依赖与执行命令 ​

需要 Bash、Go 1.27.2,以及 Redis 8.2.2 的 redis-server/redis-cli。Redis 官方归档为 redis-8.2.2.tar.gz,官方 SHA-256 为 4e340e8e822a82114b6fb0f7ca581b749fa876e31e36e9fbcb75416bec9d0608,可对照 redis-hashes。本次用 make -j4 MALLOC=libc BUILD_TLS=no OPTIMIZATION=-O2 redis-server redis-cli 编译。

把下面所有文件放在一个空目录。包装器启动只属于本次实验的临时 Redis,先核对进程 PID;测试在同一个实例内运行,结束后关闭进程并清理临时数据。不需要连接已有 Redis,不要把脚本改指向生产服务。

bash
export REDIS_BIN="$PWD/redis-8.2.2/src"
export REDIS_PORT=16389
GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org go mod download
bash with-redis.sh bash -c 'go version; go run . && go test -count=1 ./... && go test -race -count=1 ./... && go vet ./... && go mod verify'

BigCache 实验刻意等待 2.1 秒,以跨过其秒级时钟与 1 秒 LifeWindow;整个测试不是性能基准。

with-redis.sh ​

bash
#!/usr/bin/env bash
set -euo pipefail
: "${REDIS_BIN:?Set REDIS_BIN to the directory containing redis-server and redis-cli}"
export REDIS_PORT=${REDIS_PORT:-16389}
work=$(mktemp -d)
log=${REDIS_RUN_LOG:-$work/server.log}
pid=
cleanup() {
  if [[ -n "$pid" ]]; then
    kill -TERM "$pid" 2>/dev/null || true
    wait "$pid" 2>/dev/null || true
  fi
  rm -rf -- "$work"
}
trap cleanup EXIT
"$REDIS_BIN/redis-server" --bind 127.0.0.1 --port "$REDIS_PORT" \
  --protected-mode yes --save '' --appendonly no --dir "$work" \
  --daemonize no > "$log" 2>&1 &
pid=$!
echo "redis_pid=$pid port=$REDIS_PORT temporary_dir=$work" >&2
ready=0
for ((i=0;i<100;i++)); do
  kill -0 "$pid" 2>/dev/null || { cat "$log" >&2; exit 1; }
  if "$REDIS_BIN/redis-cli" -h 127.0.0.1 -p "$REDIS_PORT" INFO server 2>/dev/null | tr -d '\r' | grep -qx "process_id:$pid"; then
    ready=1; break
  fi
  sleep 0.05
done
[[ "$ready" == 1 ]] || { cat "$log" >&2; exit 1; }
"$@"

go.mod ​

go
module example.com/cachetools

go 1.27.0

require (
	github.com/allegro/bigcache/v3 v3.2.0
	github.com/dgraph-io/ristretto/v2 v2.4.2
	github.com/maypok86/otter/v2 v2.3.0
	github.com/redis/go-redis/v9 v9.23.0
	github.com/redis/rueidis v1.0.78
)

require (
	github.com/cespare/xxhash/v2 v2.3.0 // indirect
	github.com/dustin/go-humanize v1.0.1 // indirect
	github.com/stretchr/testify v1.12.1 // indirect
	go.uber.org/atomic v1.12.0 // indirect
	go.yaml.in/yaml/v3 v3.0.5 // indirect
	golang.org/x/sys v0.48.0 // indirect
)

go.sum ​

代码块 · 40 行 · 点击收起 / 展开
text
github.com/allegro/bigcache/v3 v3.2.0 h1:B45F9x3iaoBlhzIA+0jqxlThTUoyg+mOk7HUKSbJOL8=
github.com/allegro/bigcache/v3 v3.2.0/go.mod h1:qvxNn6cSKfWRmfDuPJbZcfxsQXEtoskUqPzT0kuHG5s=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgraph-io/ristretto/v2 v2.4.2 h1:x0cvjmUKxt764Yxdk2nr94we1AvPPAMh1rh5TQ+Jo80=
github.com/dgraph-io/ristretto/v2 v2.4.2/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/maypok86/otter/v2 v2.3.0 h1:8H8AVVFUSzJwIegKwv1uF5aGitTY+AIrtktg7OcLs8w=
github.com/maypok86/otter/v2 v2.3.0/go.mod h1:XgIdlpmL6jYz882/CAx1E4C1ukfgDKSaw4mWq59+7l8=
github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I=
github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
github.com/redis/go-redis/v9 v9.23.0 h1:/TTVdJa8BRNsybwwGGnmiYktHNlUXtbOQxlWpHBPL3s=
github.com/redis/go-redis/v9 v9.23.0/go.mod h1:EWP2UUk+XDaI1s5nQB5mablZN6vccXlmBQTyfnF87D0=
github.com/redis/rueidis v1.0.78 h1:hJXpEgC9IYfdwY4hCdaGYsfK+oUaAqvhI/GMy5akVJI=
github.com/redis/rueidis v1.0.78/go.mod h1:L8mnCQJJaSNL6I4pIR6Rz732HTGS9vmuXm0yT9dRvjo=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.uber.org/atomic v1.12.0 h1:BvcXdFKuviU4fTL/f+SxdQ5qJX/Jix8pAkgdUcb3XOE=
go.uber.org/atomic v1.12.0/go.mod h1:I6c4cg+6HCxRjfjSsYtApoFILnpc0CGUdGkXVqbYVNk=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=

main.go ​

代码块 · 126 行 · 点击收起 / 展开
go
package main

import (
	"context"
	"errors"
	"fmt"
	"github.com/allegro/bigcache/v3"
	"github.com/dgraph-io/ristretto/v2"
	"github.com/maypok86/otter/v2"
	"github.com/redis/go-redis/v9"
	"github.com/redis/rueidis"
	"os"
	"time"
)

func remote() error {
	port := os.Getenv("REDIS_PORT")
	if port == "" {
		return errors.New("REDIS_PORT must point to the temporary fixture")
	}
	addr := "127.0.0.1:" + port
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	r := redis.NewClient(&redis.Options{Addr: addr, MaxRetries: -1, DialTimeout: time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, ContextTimeoutEnabled: true, PoolSize: 4})
	defer r.Close()
	if err := r.Set(ctx, "lab08:tools:g", "value", time.Minute).Err(); err != nil {
		return err
	}
	v, err := r.Get(ctx, "lab08:tools:g").Result()
	if err != nil {
		return err
	}
	_, missing := r.Get(ctx, "lab08:tools:missing").Result()
	if v != "value" || !errors.Is(missing, redis.Nil) {
		return errors.New("go-redis result mismatch")
	}
	fmt.Printf("go-redis: value=%s missing=%v\n", v, errors.Is(missing, redis.Nil))
	u, err := rueidis.NewClient(rueidis.ClientOption{InitAddress: []string{addr}, DisableRetry: true, CacheSizeEachConn: 1 << 20})
	if err != nil {
		return err
	}
	defer u.Close()
	if err = u.Do(ctx, u.B().Set().Key("lab08:tools:r").Value("value").Ex(time.Minute).Build()).Error(); err != nil {
		return err
	}
	first := u.DoCache(ctx, u.B().Get().Key("lab08:tools:r").Cache(), time.Minute)
	v, err = first.ToString()
	if err != nil {
		return err
	}
	second := u.DoCache(ctx, u.B().Get().Key("lab08:tools:r").Cache(), time.Minute)
	if _, err = second.ToString(); err != nil {
		return err
	}
	if v != "value" || !second.IsCacheHit() {
		return errors.New("rueidis cache mismatch")
	}
	fmt.Printf("rueidis: value=%s second-hit=%v\n", v, second.IsCacheHit())
	return nil
}
func local() error {
	r, err := ristretto.NewCache(&ristretto.Config[string, string]{NumCounters: 1000, MaxCost: 1024, BufferItems: 64})
	if err != nil {
		return err
	}
	defer r.Close()
	queued := r.Set("oversize", "value", 10000)
	r.Wait()
	_, hit := r.Get("oversize")
	if !queued || hit {
		return errors.New("ristretto admission mismatch")
	}
	fmt.Printf("ristretto: queued=%v retained=%v\n", queued, hit)
	cfg := bigcache.DefaultConfig(time.Second)
	cfg.Shards = 1
	cfg.CleanWindow = 0
	cfg.MaxEntriesInWindow = 16
	cfg.MaxEntrySize = 64
	cfg.HardMaxCacheSize = 1
	cfg.Verbose = false
	b, err := bigcache.New(context.Background(), cfg)
	if err != nil {
		return err
	}
	defer b.Close()
	if err = b.Set("k", []byte("value")); err != nil {
		return err
	}
	time.Sleep(2100 * time.Millisecond) // Clock has seconds resolution; no cleanup writes follow.
	bv, info, err := b.GetWithInfo("k")
	if err != nil {
		return err
	}
	if string(bv) != "value" || info.EntryStatus != bigcache.Expired {
		return errors.New("bigcache expiry mismatch")
	}
	fmt.Printf("bigcache: value=%s expired=%v\n", bv, info.EntryStatus == bigcache.Expired)
	o, err := otter.New(&otter.Options[string, string]{MaximumSize: 10, ExpiryCalculator: otter.ExpiryWriting[string, string](time.Minute)})
	if err != nil {
		return err
	}
	defer o.StopAllGoroutines()
	loads := 0
	loader := otter.LoaderFunc[string, string](func(context.Context, string) (string, error) { loads++; return "value", nil })
	ctx := context.Background()
	ov, err := o.Get(ctx, "k", loader)
	if err != nil {
		return err
	}
	if _, err = o.Get(ctx, "k", loader); err != nil {
		return err
	}
	if ov != "value" || loads != 1 {
		return errors.New("otter loader mismatch")
	}
	fmt.Printf("otter: value=%s loads=%d\n", ov, loads)
	return nil
}
func main() {
	if err := remote(); err != nil {
		panic(err)
	}
	if err := local(); err != nil {
		panic(err)
	}
}

main_test.go ​

go
package main

import "testing"

func TestClientAndCacheSmoke(t *testing.T) {
	if err := remote(); err != nil {
		t.Fatal(err)
	}
	if err := local(); err != nil {
		t.Fatal(err)
	}
}

保留各书来源与许可 · 示例运行条件以正文为准